function ModAnalysis(ast)
{
  
//  function monov(value)
//  {
//    if (value instanceof JipdaValue)
//    {
//      return new JipdaValue(value.user, value.addresses().map(monoa));
//    }
//    // else Benv
//    return value.
//  }
//  
//  function monosv(storeValue)
//  {
//    return new StoreValue(monov(storeValue.aval), storeValue.fresh);
//  }
//  
  function monoa(address)
  {
    return new ContextAddress(address.base, null);
  }
  
//  function monos(store)
//  {
//    return store.entries.reduce(function (acc, entry)
//    {
//      return acc.allocAval(monoa(entry[0]), monosv(entry[1]));
//    }, new Store());
//  }
//
  function mostRecentInvocationMark(stack)
  {
    for ( var i = 0; i < stack.length; i++)
    {
      var s = stack[i];
      if (s instanceof Cont)
      {
        var marks = s.marks;
        if (marks.length > 0)
        {
          return marks[0];
        }
      }
    }
    return null;
  }

  var e = Object.create(jseval);

  e.memberCont = function(stack, store, time, c)
  {
    var mark = mostRecentInvocationMark(stack);
    if (mark !== null)
    {
      var funAddr = mark.funAddr;
      var stringPropertyName = stack[0].user;
      var propertyAccessNode = this.node;
      var baseRef = stack[1];

      var as = c.e.doScopeLookup(c.l.U_THIS, stack, this.benva, store, c);
      if (as.length !== 1)
      {
        throw new Error(as.length + " addresses (!== 1) for this: " + as);
      }
      var ths = as[0];

      baseRef.addresses().forEach(
          function(baseAddress)
          {
            var access = new ModAnalysis.Access(funAddr, ths,
                propertyAccessNode, "read", baseAddress, stringPropertyName,
                store);
            accesses.push(access);
          });
    }
    return jseval.memberCont.call(this, stack, store, time, c);
  }

  e.methodOperatorCont = function(stack, store, time, c)
  {
    var mark = mostRecentInvocationMark(stack);
    if (mark !== null)
    {
      // print("hey!", stack, this.node);
      var funAddr = mark.funAddr;
      var stringPropertyName = c.l.userLattice
          .abst1(this.node.callee.property.name);
      var propertyAccessNode = this.node.callee;
      var baseRef = stack[0];

      var as = c.e.doScopeLookup(c.l.U_THIS, stack, this.benva, store,
          c);
      if (as.length !== 1)
      {
        throw new Error(as.length + " addresses (!== 1) for this: " + as);
      }
      var ths = as[0];

      baseRef.addresses().forEach(
          function(baseAddress)
          {
            var access = new ModAnalysis.Access(funAddr, ths,
                propertyAccessNode, "read", baseAddress, stringPropertyName,
                store);
            accesses.push(access);
          });
    }
    return jseval.methodOperatorCont.call(this, stack, store, time, c);
  }

  var accesses = [];
  this.ast = ast;
  this.primLattice = new Lattice1();
  var jipda = new Jipda({
    lattice : this.primLattice,
    k : 0,
    ag : tagAg,
    evaluator : e
  });
  jipda.evalNode(this.ast);
  this.lattice = jipda.lattice;
  this.accesses = accesses;

  function lookupGlobal(name, jsa)
  {
    return jipda.globalStore.lookupAval(
        jipda.globalStore.lookupAval(jipda.globalObject).lookup(
            jsa.primLattice.abst1(name)).addresses[0]).addresses()[0];
  }
  this.globalObjectAs = [];
  this.globalObjectAs = this.globalObjectAs.addEntry(lookupGlobal("Object",
      this), createAst("function Object() {}"));
  this.globalObjectAs = this.globalObjectAs.addEntry(lookupGlobal("Function",
      this), createAst("function Function() {}"));
  this.globalObjectAs = this.globalObjectAs.addEntry(
      lookupGlobal("Array", this), createAst("function Array() {}"));  
}

// //
ModAnalysis.Access = function(funAddr, ths, accessNode, type, baseAddr, name,
    store)
{
  this.funAddr = funAddr;
  this.ths = ths;
  this.accessNode = accessNode;
  this.type = type;
  this.baseAddr = baseAddr;
  this.name = name;
  this.store = store;
}

ModAnalysis.Access.prototype.toString = function()
{
  return "(" + this.funAddr + "," + this.ths + "," + this.accessNode + ","
      + this.type + "," + this.baseAddr + "," + this.name + ", store)";
}

ModAnalysis.Access.prototype.setName =
  function (name)
  {
    return new ModAnalysis.Access(this.funAddr, this.ths, this.accessNode, this.type, this.baseAddr, name, this.store);
  }

function reverseLookup(address, store)
{
  return store.entries.flatMap(function(entry)
  {
    var aval = entry[1].aval;
    if (aval.addresses().memberAt(address) > -1)
    {
      return [ entry[0] ];
    }
    return [];
  });
}

function isPropertyAddress(address)
{
  return address.base instanceof Addr;
}

// function enclosingObject(address, store)
// {
// var ras = reverseLookup(address, store);
// var rasp = ras.filter(isPropertyAddress);
// var raspb = rasp.map(ContextAddr.base);
// return raspb.toSet();
// }

// Addr -> Store -> (Addr -> Store -> Addr*) -> Addr**
function instanceOfStarOLD(addr, store, instanceOf)
{
  var result = [];
  function helper(pref, addr)
  {
    var iofs = instanceOf(addr, store);
    if (iofs.length > 0)
    {
      return iofs.flatMap(function(iof)
      {
        if (Arrays.indexOf(iof, pref, Eq.equals) > -1)
        {
          return [ pref.addLast(addr) ];
        }
        return helper(pref.addLast(addr), iof);
      });
    }
    else
    {
      return [ pref.addLast(addr) ];
    }
  }
  ;
  return helper([], addr);
}

function instanceOfStar(addr, store, instanceOf)
{
  var todo = instanceOf(addr, store).map(function (type) {return [type]});
  var result = [];
  while (todo.length > 0)
  {
    var chain = todo[0];
    todo = todo.slice(1);
    var type = chain[chain.length - 1];
    var app = instanceOf(type, store);
    if (app.length === 0) // protoInstanceOf(Object.prototype) returns empty set
    {
      result.push(chain);
    }
    else
    {
      app.forEach(function (iof) {
        if (iof === null)
        {
          result.push(chain);
        }
        else
        {
          var newChain = chain.addUniqueLast(iof);
          if (chain.equals(newChain))
          {
            result.push(chain);
          }
          else
          {
            todo = todo.addLast(newChain);
          }
        }
      });      
    }
  }
  return result;
}

// Addr -> Addr -> Store -> (Addr -> Store -> Addr*) -> bool*
function isLocal(iof, ths, store, instanceOf)
{
  var chains = instanceOfStar(ths, store, instanceOf);
//  print("type", iof, "ths", ths, "chains", chains.join("\n"));
  var local = chains.map(function(chain)
  {
    return Arrays.indexOf(iof, chain, Eq.equals) > -1
  });
//  print("isLocal", iof, ths, chains.join("\n"), local);
  return local.toSet();
}

function filterFunction(funNode, accesses)
{
  return accesses.filter(function(access)
  {
    return access.funAddr.base === funNode.tag;
  });
}

function FunAccess(baseIof, name, local)
{
  this.baseIof = baseIof;
  this.name = name;
  this.local = local;
}
FunAccess.prototype.toString = function()
{
  return "(" + this.baseIof + "," + this.name + "," + this.local + ")";
}
FunAccess.prototype.equals = function(x)
{
  return Eq.equals(this.baseIof, x.baseIof) && Eq.equals(this.name, x.name)
      && this.local === x.local;
}

function funAccesses(funNode, trace, instanceOf)
{
  var funTas = filterFunction(funNode, trace);
  var fas = funTas.flatMap(
    function (ta)
    {
      var baseIofs = instanceOf(ta.baseAddr, ta.store);
      return baseIofs.flatMap(
        function (baseIof)
        {
          var locals = isLocal(baseIof, ta.ths, ta.store, instanceOf);
          return locals.map(function (local)
            {
              return new FunAccess(baseIof, ta.name, local);
            })
        })
    })
  return Arrays.deleteDuplicates(fas, Eq.equals);
}

function filterLocal(fas)
{
  return fas.filter(function(access)
  {
    return access.local;
  });
}

function filterForeign(fas)
{
  return fas.filter(function(access)
  {
    return !access.local;
  });
}

function atd(fas)
{
  return fas.length;
}

function atfd(fas)
{
  return filterForeign(fas).length;
}

function atld(fas)
{
  return filterLocal(fas).length;
}

function fdpSet(fas)
{
  return Arrays.deleteDuplicates(filterForeign(fas).map(function(access)
  {
    return access.baseIof;
  }), Eq.equals);
}

function ldpSet(fas)
{
  return Arrays.deleteDuplicates(filterLocal(fas).map(function(access)
  {
    return access.baseIof;
  }), Eq.equals);
}

function fdp(fas)
{
  return fdpSet(fas).length;
}

function laa(fas)
{
  return atld(fas) / atd(fas);
}

// ////////////////
function addrToNode(ast) 
{ 
  return function(address)
    {
      if (isFinite(address.base)) // address directly corresponding to node?
      {
        var tagNode = tagToNode(ast)(address.base);
//        if (isCallExpression(tagNode)) // TODO HACK: should/can(?) be performed in ag 
//        {
//          return tagNode.callee.object;
//        }
        return tagNode;
      }
      return false;
    }
}

function dependencyStar(bs)
{
  var changed = true;
  while (changed)
  {
    changed = false;
    for (var i = 0; i < bs.length - 1; i++)
    {
      var source = bs[i];
      for (var j = i + 1; j < bs.length; j++)
      {
        var target = bs[j];
        var c = source.concat(target).toSet();
        if (c.length !== source.length + target.length)
        {
          changed = true;
          bs = bs.slice(0, i).addLast(c).concat(bs.slice(i+1,j)).concat(bs.slice(j+1));
        }
      }
    }
  }
  return bs;
}

function bucketIndexOf(x, buckets)
{
  for ( var i = 0; i < buckets.length; i++)
  {
    if (Arrays.indexOf(x, buckets[i], Eq.equals) > -1)
    {
      return i;
    }
  }
  return -1;
}

/*
 * type based on [[Prototype]] of object
 */
function protoInstanceOf(address, store)
{
  var object = store.lookupAval(address);
  var protoAddresses = object.Prototype.addresses();
  return protoAddresses;
}

/*
 * object receives type based on allocation site
 */
function addrInstanceOf(address, store)
{
  return [address];
}

/*
 * object receives type based on allocation site, execept newed objects that get [[Prototype]] as type
 */
function instanceOf2(ast)
{ 
  return function (address, store)
    {
      var node = addrToNode(ast)(address);
      if (node)
      {
        if (isNewExpression(node) || isArrayExpression(node) || isCallExpression(node))
        {
          var object = store.lookupAval(address);
          var protoAddresses = object.Prototype.addresses();
          return protoAddresses;
        }
        
      }
      return [address];
    }
}

function instanceOf3(instanceOf, flowsToEq)
{
  return function (address, store)
  {
    var type = instanceOf(address, store);
    var typeEq = bucketIndexOf(type[0], flowsToEq);
//    print("address ->", type, "->", typeEq, "->", flowsToEq[typeEq]);
    if (typeEq > -1)
    {
      return flowsToEq[typeEq];      
    }
    return type;
  }
}

function instanceOf4(instanceOf, flowsToEq, interf, ast)
{
  var interfEq = flowsToEq.flatMap(
    function (equiTypes)
    {
      var interfaces = equiTypes.map(interf);
      var typeInterfaces = interfaces.reduce(function (acc, x, i) {return acc.addLast({type:equiTypes[i],interf:x})}, []);
      print("equiTypes", equiTypes, "interfaces", interfaces, "typeInterfaces", typeInterfaces);
      var redInterfaces = Arrays.reduceFixpoint(typeInterfaces, function (x, y) {
        var xsy = x.interf.subsumes(y.interf);
        var ysx = y.interf.subsumes(x.interf);
        if (xsy)
        {
          return {type:x.type, interf:x.interf};
        }
        if (ysx)
        {
          return {type:y.type, interf:y.interf};
        }
//        print("x", x.type, x.interf, "y", y.type, y.interf, "xsy", xsy, "ysx", ysx);
      });
      var part = redInterfaces.map(function (x) { return x.type});
      print("redInterfaces", redInterfaces, "part", part);
      return [equiTypes];
    });
  print("interfEq", interfEq);
  return function (address, store)
  {
    var types = instanceOf(address, store);
    return types.flatMap(
      function (type)
      {
        var typeEq = bucketIndexOf(type, interfEq);
        if (typeEq > -1)
        {
          return interfEq[typeEq];      
        }
        return type;            
      }).toSet();
  };
}


function flowsTo(accs, instanceOf)
{
  var baseNodeToType = accs.reduce(
    function (acc, el)
    {
      var address = el.baseAddr;
      var store = el.store;
      instanceOf(address, store).forEach(function (type) {acc = acc.updateSetEntry(el.accessNode.object, type)});
      return acc;
    }, []);
  // initial buckets are types, grouped by baseNode
  var initialTypeBuckets = baseNodeToType.map(function (bna) {return bna[1]});
  var typeBuckets = dependencyStar(initialTypeBuckets);
  return typeBuckets;
}

function typeToInterface(accs, instanceOf)
{
  var typeToNames = accs.reduce(
    function (acc, el)
    {
      var address = el.baseAddr;
      var store = el.store;
      instanceOf(address, store).forEach(function (type) {acc = acc.updateSetEntry(type, el.name)});
      return acc;
    }, []);
  return function(type) {return typeToNames.getSetEntry(type)};
}

//note: assumes that array prototype is unchanged
function filterArrayAccesses(fas)
{
  return fas.filter(function(fa)
  {
    if (fa.baseIof.toString().equals("Array.prototype@0"))
    {
//      if (lattice.isStringArrayIndex(fa.name))
//      {
//        print("rejecting", fas);
        return false;
//      }
    }
    return true;
  });
}

function mapArrayIndexes(trace, lattice)
{
  return trace.map(
    function(el)
    {
      if (lattice.isStringArrayIndex(el.name))
      {
        return el.setName("IDX"); // 'special' name
      }
      return el;
    });
}

// ////////////////////////////////////

function T1(fun, accs)
{
  var fas = funAccesses(fun, accs, protoInstanceOf);
  return fas;
}

function T2(fun, accs, ast)
{
  var fas = funAccesses(fun, accs, addrInstanceOf);
  return fas;
}

function T3(fun, accs, ast)
{
  var fas = funAccesses(fun, accs, instanceOf2(ast));
  return fas;
}

//function T4(fun, accs, ast)
//{
//  var flowsToEq = flowsTo(accs, instanceOf2(ast));
//  print("buckets", flowsToEq);
//  var fas = funAccesses(fun, accs, instanceOf3(instanceOf2(ast), flowsToEq));
//  return fas;
//}
//
//function T5(fun, accs, ast)
//{
//  var flowsToEq = flowsTo(accs, instanceOf2(ast));
//  print("buckets", flowsToEq);
//  var interf = typeToInterface(accs, instanceOf2(ast));
//  var fas = funAccesses(fun, accs, instanceOf4(instanceOf2(ast), flowsToEq, interf, ast));
//  return fas;
//}
//
//function T6(fun, trace, ast, lattice)
//{
//  var trace2 = mapArrayIndexes(trace, lattice);
////  var trace3 = 
//  var flowsToBuckets = flowsToEq(trace2, ast);
//  print("buckets", flowsToBuckets.join("\n"));
//  var fas = funAccesses(fun, trace2, bucketInstanceOf(flowsToBuckets, ast));
//  return fas;
//}

//////////////////////////////////////

//function T2(fun, trace, ast, lattice)
//{
//var fas = funAccesses(fun, trace, instanceOf2(ast));
//var fas2 = filterArrayAccesses(fas, lattice);
//return fas2;
//}
//
//function T3(fun, trace, ast, lattice)
//{
//var sas = singleStaticType(tas, ast, instanceOf2(ast));
//var fas = funAccesses(fun, sas);
//var fas2 = filterArrayAccesses(fas, lattice);
//return fas2;
//}




